package com.xiyuan.config;

import com.xiyuan.SysI18n;
import com.xiyuan.smartutils.*;
import com.xiyuan.smartutils.constants.CodeConstants;
import com.xiyuan.smartutils.constants.SysConstants;
import com.xiyuan.smartutils.constants.XmlConstants;
import com.xiyuan.smartutils.xml.Xmls;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;


/**
 * 工程参数配置类，对应conf目录下一个.xml文件，如boot.xml<br><br>
 * 每个配置类下有一个名称如boot.xml的配置文件<br>
 * 
 * 支持两种格式：
 * 1）默认格式xml，每个配置类下有0-n个group（配置组），每个group下有0-n个item（配置项）
 * 2）配置通用格式，如Windows的ini文件，Linux下的[section]配置文件
 * 
 * @version v1.0.0 @author lgz 2017-8-27 新建与整理
 */
public class Config implements Serializable, SysConstants, CodeConstants, XmlConstants
{
    private static final long serialVersionUID = 1L;
    
    private final String name;
    private final String path;
    private final boolean isXml;
    private final boolean ignoreCase;
    
    private LinkedHashMap<String, Group> groupMap = new LinkedHashMap<>();

    /********************************************************************/
    //构造函数，默认（XML格式，不忽略大小写）
    /********************************************************************/
    
    public Config(String name)
    {
        this(name, null, true, false);
    }
    
    public Config(String name, String path)
    {
        this(name, path, "xml".equals(Files.getFileExt(path)), false);
    }
    
    public Config(String name, String path, boolean isXml, boolean ignoreCase)
    {
        this.name = name;
        this.path = path;
        this.isXml = isXml;
        this.ignoreCase = ignoreCase;
    }
    
    /********************************************************************/
    //以下为Config运行时生成字符串、设置值和加载保存
    /********************************************************************/
    
    public String toString()
    {
        if (isXml())
        {//XML格式
            StringBuilder strb = new StringBuilder();
            strb.append(_XML_DEFIND_).append(_BR_);
            strb.append(CONFIG_XML_DOCTYPE).append(_BR_);
            strb.append(CONFIG_XML_HEAD).append(_BR_).append(_BR_);
            
            for (Group group : groupMap.values())
            {
                strb.append(group.toString()).append(_BR_).append(_BR_);
            }
            
            strb.append(CONFIG_XML_TAIL);
            return strb.toString();
        }
        else
        {//INI格式
            StringBuilder strb = new StringBuilder();
            for (Group group : groupMap.values())
            {
                strb.append(group.toString()).append(_BR_);
            }
            return strb.toString();
        }
    }
    
    /** 根据path加载XML或INI */
    public Config load() throws IOException
    {
        if (Validates.isBlank(path))
            return this;
        
        try(FileInputStream in = new FileInputStream(path))
        {
            return load(in);
        }
    }
    
    /** 未指定path加载流 */
    public Config load(InputStream in) throws IOException
    {
        if (isXml())
        {
            Document document = Xmls.buildDocument(in, new ConfigResolver(path));
            NodeList groupNodeList = document.getElementsByTagName(GROUP);
            
            //处理组
            initGroup(groupNodeList);
        }
        else
        {
            //至少有三个字节，格式如[1]，否则认为格式不对[groupId]
            Asserts.as(in.available() >= 3, SysI18n.configFileFormatNotCorrect, path);
            
            //先读取一行，判断是否有BOM
            byte[] buffer = new byte[4096];
            int len = IoUtils.readLine(in, buffer, buffer.length);
            String encoding = null;
            List<String> lineList = new ArrayList<String>();
            
            do
            {
                if (encoding == null)
                    encoding = IoUtils.getStreamEncoding(buffer, len);
                
                String str = StrUtils.trim(new String(buffer, 0, len, encoding));
                if (!Validates.isEmpty(str))
                    lineList.add(str);
            }
            while ((len = IoUtils.readLine(in, buffer, buffer.length)) != -1);
            
            //处理组
            initGroup(lineList);
        }
        
        return this;
    }
    
    /** 初始化配置信息 */
    private void initGroup(List<String> lineList) throws IllegalArgumentException
    {
        Group group = null;char symbol = '#';String desc = null;
        for (String line : lineList)
        {
            if (line.startsWith("["))
            {//配置组
                int ind = line.indexOf("]");
                if (ind == -1)
                    throw Asserts.exception("初始化配置文件[%s]时本页配置组[groupId]不正确", path);
                
                String groupId = line.substring(1, ind);
                group = new Group(this, groupId, desc, symbol);
                if (!addGroup(group))
                    throw Asserts.exception("初始化配置文件[%s]时本页有相同的配置组[%s]", path, groupId);
            }
            else if (line.startsWith("#") || line.startsWith(";"))
            {//注释
                symbol = line.charAt(0);
                desc = line.substring(1).trim();
            }
            else
            {//配置项
                if (group == null)
                    throw Asserts.exception("初始化配置文件[%s]时本页配置项[%s]前面没有[groupId]", path, line);
                
                int ind = line.indexOf("=");
                if (ind < 1)
                    throw Asserts.exception("初始化配置文件[%s]时本页配置项[%s]没有=号或=号是第一个字符", path, line);
                
                String key = line.substring(0, ind).trim();
                String value = StrUtils.trimLeft(line.substring(ind+1));
                ItemType type = ItemType.PROTECTED;
                if (desc != null)
                {
                    if (desc.startsWith(_PROTECTED_)){
                        desc = desc.substring(_PROTECTED_.length());
                    }else if (desc.startsWith(_PRIVATE_)){
                        type = ItemType.PRIVATE;
                        desc = desc.substring(_PRIVATE_.length());
                    }else if (desc.startsWith(_PUBLIC_)){
                        type = ItemType.PUBLIC;
                        desc = desc.substring(_PUBLIC_.length());
                    }
                }
                
                if (!group.add(key, value, type, desc, symbol))
                    throw Asserts.exception("初始化配置文件[%s]时组[%s]有相同的配置项[%s]", path, group.getId(), key);
            }
        }
    }
    
    /** 初始化配置信息 */
    private void initGroup(NodeList groupNodeList) throws IllegalArgumentException
    {
        // 遍历<config>标签
        for (int i=0;i<groupNodeList.getLength();i++)
        {
            Node node = groupNodeList.item(i);
            if (node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            
            String groupId = Xmls.getAttribute(node, ID);
            String groupDesc = Xmls.getAttribute(node, DESC);
            
            Group group = new Group(this, groupId, groupDesc);
            if (!addGroup(group))
                throw Asserts.exception("初始化配置文件[%s]时本页有相同的配置组[%s]", path, groupId);
            
            NodeList childNodeList = node.getChildNodes();
            for (int j=0;j<childNodeList.getLength();j++)
            {
                Node childNode = childNodeList.item(j);
                if (childNode.getNodeType() != Node.ELEMENT_NODE)
                    continue;
                
                NamedNodeMap namedNodeMap = childNode.getAttributes();
                String key = Xmls.getAttribute(namedNodeMap, KEY);
                String value = Xmls.getAttribute(namedNodeMap, VALUE);
                String desc = Xmls.getAttribute(namedNodeMap, DESC);
                String type = Xmls.getAttribute(namedNodeMap, TYPE);
                
                if (!group.add(key, value, ItemType.toType(type), desc))
                    throw Asserts.exception("初始化配置文件[%s]时组[%s]有相同的配置项[%s]", path, groupId, key);
            }
        }
    }
    
    /** 保存XML，强制UTF-8并加BOM */
    public Config save() throws IOException
    {
        if (Validates.isBlank(path))
            return this;
        
        String content = toString();
        
        try(FileOutputStream fos = new FileOutputStream(path))
        {
            fos.write(0xEF);fos.write(0xBB);fos.write(0xBF);//加上UTF-8的BOM，保证Windows和Linux都能识别
            fos.write(content.getBytes(_UTF_8_C_));
            fos.flush();
            return this;
        }
    }
    
    /********************************************************************/
    //以下为Config配置组和配置项属性
    /********************************************************************/
    
    /** 增加组，最简单的只有id的 */
    public boolean addGroup(String groupId)
    {
        return addGroup(new Group(this, groupId, null));
    }
    
    /** 增加组，有描述的 */
    public boolean addGroup(String groupId, String groupDesc)
    {
        return addGroup(new Group(this, groupId, groupDesc));
    }
    
    /** 增加组，有描述的且指定描述注释符的 */
    public boolean addGroup(String groupId, String groupDesc, char symbol)
    {
        return addGroup(new Group(this, groupId, groupDesc, symbol));
    }
    
    /** 增加组 */
    public boolean addGroup(Group group)
    {
        String groupIdCase = ignoreCase(group.getId());
        if (groupMap.containsKey(groupIdCase))
            return false;
        
        groupMap.put(groupIdCase, group);
        return true;
    }
    
    public void removeGroup(String groupId)
    {
        groupMap.remove(groupId);
    }
    
    /** 通过id和key设置String */
    public void setString(String id, String key, String value)
    {
        String groupIdCase = ignoreCase(id);
        Group group = groupMap.get(groupIdCase);
        if (group == null)
        {//不存在组
            group = new Group(this, id, null);
            group.add(key, value);
            groupMap.put(groupIdCase, group);
            return;
        }
        else
        {//存在组
            Item item = group.item(key);
            if (item == null)
                group.add(key, value);
            else
                item.setValue(value);
        }
    }
    
    /** 通过id和key设置int */
    public void setInt(String id, String key, int value)
    {
        setString(id, key, String.valueOf(value));
    }
    
    /** 通过id和key设置long */
    public void setLong(String id, String key, long value)
    {
        setString(id, key, String.valueOf(value));
    }
    
    /** 通过id和key设置boolean */
    public void setBoolean(String id, String key, boolean value)
    {
        setString(id, key, String.valueOf(value));
    }

    /**
     * 删除一个配置项
     *
     * @param id        组号
     * @param key       配置键
     * @return          =true表示删除成功,=false表示无配置项
     */
    public boolean remove(String id, String key)
    {
        Group group = groupMap.get(id);
        if (group == null)
            return false;
        
        return group.remove(key);
    }
    
    /********************************************************************/
    //以下为Config获取配置组方法
    /********************************************************************/
    
    /** 是否有配置组 */
    public boolean hasGroup(String id)
    {
        return groupMap.containsKey(ignoreCase(id));
    }
    
    /** 获取配置组 */
    public Group getGroup(String id)
    {
        return groupMap.get(ignoreCase(id));
    }
    
    /** 获取配置组表 */
    public LinkedHashMap<String, Group> getGroupMap()
    {
        return groupMap;
    }
    
    /** 获取配置组列表 */
    public Collection<Group> getGroupList()
    {
        return groupMap.values();
    }
    
    /** 判断是否有配置项 */
    public boolean hasItem(String id, String key)
    {
        Group group = groupMap.get(id);
        if (group == null)
            return false;
        
        return group.hasItem(key);
    }
    
    /**
     * 通过指定配置组的条目表
     * 
     * @param id 配置组编码
     * @return 配置组下条目表
     */
    public LinkedHashMap<String, Item> getItemMap(String id)
    {
        return hasGroup(id)?getGroup(id).map():null;
    }
    
    /**
     * 通过指定配置组的条目列表
     * 
     * @param id 配置组编码
     * @return 配置组下条目列表
     */
    public Collection<Item> getItemList(String id)
    {
        return hasGroup(id)?getGroup(id).list():null;
    }
    
    /** 通过id, key获取String */
    public String getString(String id, String key)
    {
        Group group = getGroup(id);
        return (group == null)?null:group.getString(key);
    }
    
    /** 通过id, key获取String,如果为null,则返回缺省值 */
    public String getString(String id, String key, String defaultValue)
    {
        String value = getString(id, key);
        return (value == null)?defaultValue:value;
    }
    
    /** 通过id, key获取int型,如果null,返回-1 */
    public int getInt(String id, String key)
    {
        String value = getString(id, key);
        return (value == null)? -1: Ints.toInt(value, -1);
    }
    
    /** 通过id, key获取int型,如果为null,则返回缺省值 */
    public int getInt(String id, String key, int defaultValue)
    {
        int value = getInt(id, key);
        return (value == -1)?defaultValue:value;
    }
    
    /** 通过id, key获取int型,如果null,返回-1 */
    public long getLong(String id, String key)
    {
        String value = getString(id, key);
        return (value == null)? -1: Longs.toLong(value, -1);
    }
    
    /** 通过id, key获取int型,如果为null,则返回缺省值 */
    public long getLong(String id, String key, long defaultValue)
    {
        long value = getLong(id, key);
        return (value == -1)?defaultValue:value;
    }
    
    /** 通过id, key获取boolean型,如果为null,则返回缺省值 */
    public boolean getBoolean(String id, String key, boolean defaultValue)
    {
        String value = getString(id, key);
        return (value == null)?defaultValue:"true".equalsIgnoreCase(value);
    }
    
    /** 是否为真 */
    public boolean isTrue(String id, String key)
    {
        return "true".equalsIgnoreCase(getString(id, key));
    }
    
    /** 是否为假 */
    public boolean isFalse(String id, String key)
    {
        return "false".equalsIgnoreCase(getString(id, key));
    }
    
    /********************************************************************/
    //以下为Config基本属性
    /********************************************************************/
    
    public String ignoreCase(String value)
    {
        return ignoreCase?value.toLowerCase():value;
    }
    
    public String getName()
    {
        return name;
    }
    
    public String getPath()
    {
        return path;
    }
    
    public boolean isIgnoreCase()
    {
        return ignoreCase;
    }
    
    public boolean isXml()
    {
        return isXml;
    }
    
    public boolean isIni()
    {
        return !isXml;
    }
}
